feat(nix): migrate the devcontainer to a Nix toolchain + Claude-native setup (#625)#670
Open
c-vigo wants to merge 155 commits into
Open
feat(nix): migrate the devcontainer to a Nix toolchain + Claude-native setup (#625)#670c-vigo wants to merge 155 commits into
c-vigo wants to merge 155 commits into
Conversation
Replace the cursor-agent invocations in the worktree recipes with the claude CLI. worktree-start/worktree-attach now launch 'claude --dangerously-skip-permissions' in the tmux session, mapping 'agent chat --yolo --approve-mcps' (autonomous, auto-approve shell and MCP prompts) onto claude's permission-bypass flag. The cursor-specific directory-trust helper and the 'tmux send-keys a' approval trigger are no longer needed and are removed. Prerequisite, auth (claude auth status/login, ANTHROPIC_API_KEY), requirements.yaml, and the generated docs now reference the claude CLI. Refs: #627
Replace Debian/FHS-specific assertions in tests/test_image.py with path-agnostic equivalents so the suite is valid against both the current Debian image and the future Nix image: - convert dpkg host.package(...).is_installed checks (git, curl, openssh-client, nano, tmux, rsync) to --version/-V runs - resolve gh, just, hadolint, taplo and cargo-installed tools via PATH (command -v) instead of hardcoded /usr/local/bin, /root/.cargo/bin and /root/.local/bin locations - drop the DEBIAN_FRONTEND env assertion and apt-sourced version-prefix checks (git, curl, tmux, rsync) from EXPECTED_VERSIONS Refs: #635
Move agent rules and skills from .cursor/ to .claude/ and delete the root .cursor/ directory: - Move the 30 skills to .claude/skills/ and rewrite the 29 .claude/commands/*.md wrappers to point at the new paths and .claude-located rules. - Split the 7 .cursor/rules/*.mdc: static principles (coding-principles, commit-messages, changelog, single-source-of-truth) are consolidated into CLAUDE.md; workflow rules (branch-naming, tdd, subagent-delegation) become on-demand .claude/skills/. - Port agent-models.toml and worktrees.json to .claude/. - Update path consumers: docs/generate.py scan path, the check-skill-names and generate-docs pre-commit hooks, the check-skill-names and derive-branch-summary shell entrypoints, scripts/manifest.toml, docs/SKILL_PIPELINE.md(.j2), docs/RELEASE_CYCLE.md, CLAUDE.md command table, CODEOWNERS, label-taxonomy.toml, and the vig-utils README/tests. The downstream assets/workspace/.cursor/ template is left for #629. Refs: #626
## Summary Implements C1 (#626): make `.claude/` the single source of truth for agent rules and skills, removing the `.cursor/` indirection. - **Skills:** moved the 30 `.cursor/skills/*/SKILL.md` (and the `inception_explore/README.md` sibling) to `.claude/skills/`; rewrote the 29 `.claude/commands/*.md` wrappers to point at `.claude/skills/...` and `.claude/`-located rules. - **Rules split** (7 `.mdc`): static principles (`coding-principles`, `commit-messages`, `changelog`, `single-source-of-truth`) consolidated into `CLAUDE.md`; workflow rules (`branch-naming`, `tdd`, `subagent-delegation`) became on-demand `.claude/skills/` with `disable-model-invocation: true`. - **Config:** ported `agent-models.toml` and `worktrees.json` to `.claude/`. - **Path consumers updated:** `docs/generate.py` scan path + docstrings; `.pre-commit-config.yaml` (`check-skill-names` entry/`files`, `generate-docs` `files` now also fires on `.claude/skills/**/SKILL.md` — folds in #144); `check-skill-names.sh` and `derive-branch-summary.sh` defaults; `scripts/manifest.toml`; `docs/SKILL_PIPELINE.md(.j2)`; `docs/RELEASE_CYCLE.md`; `CLAUDE.md` command table + rule pointers; `.github/CODEOWNERS`; `.github/label-taxonomy.toml`; vig-utils `README.md` and tests. - **Deleted** the root `.cursor/`. ## TDD - `packages/vig-utils/tests/test_claude_ssot.py` asserts no tracked file (outside `assets/workspace/`, archival `docs/issues|pull-requests|plans/`, and append-only `CHANGELOG.md`) references `.cursor/skills/`, and that root `.cursor/` is gone. Committed failing first (RED), then implementation (GREEN). ## Verification - All 492 vig-utils tests pass. - `pre-commit run --all-files` green. - `just docs` regenerates cleanly; command table intact (33 rows); the 3 new workflow skills do not leak into the table. - Every command wrapper resolves to an existing `.claude/skills/.../SKILL.md`; no `.cursor/` path in any wrapper; root `.cursor/` deleted. ## Coordination notes - The downstream `assets/workspace/.cursor/` template is **#629 (C4)'s** scope and is intentionally left in place. The mandatory `sync-manifest` pre-commit hook now also propagates `.claude/skills/` + `.claude/worktrees.json` into `assets/workspace/.claude/`; the stale `assets/workspace/.cursor/` tree is for #629 to remove. - `justfile.worktree` recipe bodies were not edited (C2/W2). The `~/.cursor/cli-config.json` reference in `SKILL_PIPELINE.md` is the cursor-agent CLI runtime (#627) and was left untouched. - #144 is effectively resolved by the updated `generate-docs` hook filter. Refs: #626
…' into feature/627-worktree-claude-cli # Conflicts: # CHANGELOG.md # assets/workspace/.devcontainer/CHANGELOG.md
…' into feature/635-portable-testinfra # Conflicts: # CHANGELOG.md # assets/workspace/.devcontainer/CHANGELOG.md
## Summary Implements C2 (#627): replaces the `cursor-agent` CLI with the `claude` CLI throughout the worktree pipelines. This is a functional change — the recipes now drive `claude` end-to-end. ### Changes - **`justfile.worktree`** (+ synced template `assets/workspace/.devcontainer/justfile.worktree`, regenerated via `sync-manifest`): - Prereq check `command -v agent` → `command -v claude` (install hint → `npm install -g @anthropic-ai/claude-code`). - Auth block `agent status`/`agent login`/`CURSOR_API_KEY` → `claude auth status`/`claude auth login`/`ANTHROPIC_API_KEY`. - tmux launches `agent chat --yolo --approve-mcps "$PROMPT"` (and no-prompt `agent chat --approve-mcps`) → `claude --dangerously-skip-permissions "$PROMPT"` / `claude --dangerously-skip-permissions`, in all three sites (existing-worktree, new-worktree, attach-restart). - Removed the cursor-specific `_wt_ensure_trust` helper + calls and the `tmux send-keys "a"` approval trigger (no longer needed — see decisions). - **`scripts/requirements.yaml`**: `agent` (cursor.com installer) entry → `claude` entry (`command -v claude`, `claude --version`, `npm install -g @anthropic-ai/claude-code`). - **Docs**: `README.md` / `CONTRIBUTE.md` recipe help regenerated; `docs/templates/SKILL_PIPELINE.md.j2` (and generated `docs/SKILL_PIPELINE.md`) updated to describe the `claude` runtime. - **`tests/bats/worktree-claude-cli.bats`**: lightweight recipe-grep tests (no `cursor-agent`/`agent chat` survives; `claude` is driven; prereq checks `claude`). The full `worktree.bats` functional rewrite is C5's (#630) — not done here. - **`CHANGELOG.md`** Unreleased / Changed entry. ### Decisions (flag-mapping) - `--yolo` (auto-approve shell commands) **and** `--approve-mcps` (auto-approve MCP servers) both map onto **`claude --dangerously-skip-permissions`**, which bypasses all permission and MCP approval prompts — the autonomous, no-human-at-terminal equivalent. - **tmux pattern kept**: the detached `tmux new-session` driving is retained (so `worktree-attach`, list/stop/clean lifecycle and idle/dashboard features still work). The prompt is now passed as a positional arg to `claude` at launch instead of via the interactive session, so the post-launch `send-keys "a"` accept-trigger (which dismissed cursor-agent's trust prompt) is **removed** — `--dangerously-skip-permissions` means there is no prompt to accept. - **Trust helper removed**: `_wt_ensure_trust` wrote `~/.cursor/cli-config.json` `trustedDirectories`, which is cursor-agent-only and unread by `claude`; with `--dangerously-skip-permissions` directory trust is moot, so it is dead code and was dropped. `jq` requirement stays (still used for issue-metadata parsing). - Did **not** touch `_read_model`/`.cursor/agent-models.toml` or `.cursor/rules` paths (W1/C1's domain). ### Verification - `grep -n 'cursor-agent\|agent chat' justfile.worktree assets/workspace/.devcontainer/justfile.worktree` → empty. - `scripts/requirements.yaml` lists `claude`, not `agent`. - `just precommit` → all hooks green (including `sync-manifest`, `generate-docs`, `shellcheck`, `just` format). - `just --list` parses the worktree recipes; help text shows "launch the claude CLI". Refs: #627
…' into feature/635-portable-testinfra # Conflicts: # CHANGELOG.md # assets/workspace/.devcontainer/CHANGELOG.md
…es (#645) ## Summary Makes `tests/test_image.py` path-agnostic (T2.2) so the testinfra suite stays valid against **both** the current Debian image and the future Nix image. Pure test refactor — no `conftest.py` image source/name changes. - Convert dpkg `host.package(...).is_installed` checks (`git`, `curl`, `openssh-client`, `nano`, `tmux`, `rsync`) to `--version`/`-V` runs via a small `assert_tool_runs` helper. - Resolve `gh`, `just`, `hadolint`, `taplo` and cargo-installed tools via PATH (`command -v`, new `assert_tool_on_path` helper) instead of hardcoded `/usr/local/bin`, `/root/.cargo/bin`, `/root/.local/bin`. - Drop the `DEBIAN_FRONTEND` env assertion and the apt-sourced version-prefix checks (`git`, `curl`, `tmux`, `rsync`) from `EXPECTED_VERSIONS`. - Reviewed the `/root/assets/workspace/` mount assertions: they use POSIX `host.file(...)` checks on image-controlled `/root/...` paths (not Debian-FHS/dpkg), so they are already portable and left functionally intact. Note: bumped the manually-installed `just` version pin `1.53.` -> `1.54.` to match the current latest-release binary in the image (the old pin was already failing on a fresh build, independent of this refactor). ## Verification - Built the Debian image with isolated tag `wt635` (`./scripts/build.sh wt635 ...`), no `:latest`/`:dev` clobber, nothing pushed. - `just test-image wt635`: **64 passed in 193.32s** (green). - `just precommit`: all hooks pass. - No Debian/FHS-only assertions remain (no `is_installed`/`host.package`, no hardcoded `/usr/local/bin` etc., no `DEBIAN_FRONTEND`, no apt version prefixes) — remaining textual matches are docstrings/comments only. Refs: #635
Delete the unpinned cursor.com/install build step and its /root/.local/bin PATH entry, leaving an all-nixpkgs toolchain ahead of the Nix migration. Refs: #628
Remove test_cursor_agent_installed, which asserts a feature removed in the same change; keeps the suite coherent and green. Refs: #628
Remove the CVE-2026-55388 (piscina) .trivyignore entry, which only existed for the now-removed cursor-agent CLI. Refs: #628
Replace the stale assets/workspace/.cursor/ template tree with the Claude-native .claude/ payload (skills, agent-models.toml, worktrees.json) via the sync manifest, and drop Cursor editor glue: - Remove assets/workspace/.cursor/; add .claude/agent-models.toml to the workspace sync manifest so the template carries the same payload the old .cursor/ template did. - Drop the cursor-remote-ssh socket glob from verify-auth.sh. - Drop the command -v cursor fallback in justfile.devc open recipe (VS Code only). - COMMIT_MESSAGE_STANDARD.md: VS Code / Cursor -> VS Code. Refs: #629
## C4 #629 — Migrate templates & editor glue off Cursor (VS Code only) Reconciles the workspace template left behind by C1 (#626) and removes the remaining Cursor **editor** glue in #629's scope. Decision: **VS Code only**. ### Changes - **Template `.cursor/` → `.claude/`**: deleted the stale `assets/workspace/.cursor/` tree. C1's sync-manifest hook already propagated `.claude/skills/` and `.claude/worktrees.json`; this PR adds `.claude/agent-models.toml` to `scripts/manifest.toml` so the template carries the **same payload the old `.cursor/` template did** (skills + agent-models.toml + worktrees.json). The template's synced `subagent-delegation` skill references `agent-models.toml`, so this also fixes a dangling reference C1 left. - **`verify-auth.sh`**: dropped the `/tmp/cursor-remote-ssh-*.sock` glob from the SSH-agent socket scan. - **`justfile.devc`**: `open` recipe now launches VS Code only (removed the `command -v cursor` fallback). - **`docs/COMMIT_MESSAGE_STANDARD.md`** (root SSoT + synced template copy): "VS Code / Cursor" → "VS Code". - **CHANGELOG**: added an `## Unreleased` Changed entry. ### Scope boundaries (intentionally NOT touched) - `justfile.worktree` and `solve-and-pr/SKILL.md` `cursor-agent` references → owned by **#627** (worktree pipelines cursor-agent → claude); they sync from the root SSoT, so editing them here would collide with #627. - `.github/agent-blocklist.toml` "cursor" entries → AI commit-identity blocklist, explicitly **kept** per **#630**. Not editor glue. - `setup-git-conf.sh` had no remaining `cursor` references on the epic tip. ### TDD - RED: `tests/bats/init-workspace.bats` — assert template scaffolds `.claude/` (+ `skills/`), does NOT scaffold `.cursor/`, and carries no `cursor-remote` / `command -v cursor` glue. Committed failing first. - GREEN: implementation makes all four pass; full `init-workspace.bats` suite 27/27 green. - Build-free rationale: the integration `initialized_workspace` fixture requires a container image build (out of bounds this batch). `init-workspace.sh` rsyncs `assets/workspace/` verbatim, so structural assertions on the template tree are a faithful proxy for what new workspaces scaffold. ### Verification - `just precommit` — all hooks pass (sync-manifest, check-skill-names, generate-docs, shellcheck, taplo, etc.). - `grep -rn 'cursor' assets/workspace justfile.devc` (excluding CHANGELOG prose) returns only the out-of-scope #627/#630 hits listed above. - No `assets/workspace/.cursor/` remains. Refs: #629
Removes the unpinned `cursor-agent` CLI install from the devcontainer image and the CVE ignore tied to it. - Delete the cursor-agent install block from `Containerfile` and `build/Containerfile` (+ dead PATH note) - Drop `CVE-2026-55388` (piscina, bundled in cursor-agent) from `.trivyignore` - Remove the now-invalid `test_cursor_agent_installed` image test (atomically coupled to the install removal; pre-empts that one bullet of C5 #630) - Changelog entry Verification deferred to CI (image suite): build + image tests + Trivy security scan. Local build was not completed in-worktree. Refs: #628
…650) ## Summary Implements C5 (#630) of the Nix/Claude migration epic (#625): adapts the Cursor-coupled worktree test to the `claude` CLI and locks in the AI-identity blocklist so removing Cursor never weakens the org "never name an AI in history" control. - **`tests/bats/worktree.bats`** — replaced the `send-keys 'a' approves agent trust prompt` case (which skipped on `command -v agent` and asserted `"Cursor Agent"`) with `claude CLI launches in tmux without an interactive trust prompt`: skips on `command -v claude`, launches `claude --dangerously-skip-permissions --version` the way the migrated recipes do, and asserts no trust prompt stalls the pane. No assertion expecting `cursor-agent`/"Cursor Agent" remains. - **Blocklist regression test** — added `TestCanonicalBlocklist` in `packages/vig-utils/tests/test_check_pr_agent_fingerprints.py` exercising the REAL `.github/agent-blocklist.toml` (not a mock): a parametrized test proves BOTH `cursor` and `claude` are rejected in a commit message, plus a guard that both names stay in the blocklist. The `cursor` entries are intentionally kept. - **CHANGELOG** — `## Unreleased` Changed entry for #630. ## Reconciliation with C2 (#627) C2 added `tests/bats/worktree-claude-cli.bats` (static recipe-grep: justfiles drive `claude`, no `cursor-agent` survives). That file is kept as-is — it covers the recipe layer. This PR covers the complementary runtime/behavioral layer in `worktree.bats`, so there is no duplication. C3 (#628) already removed `test_cursor_agent_installed`; not touched. ## Verification - `just precommit` — all hooks pass. - `uv run pytest` (vig-utils) — 495 passed (incl. 3 new blocklist tests). - `CI=true npx bats tests/bats/worktree.bats tests/bats/worktree-claude-cli.bats` — 13 passed (tmux integration cases skip under CI by design). - Blocklist test confirmed proving cursor + claude both rejected against the canonical TOML. Done via TDD: blocklist regression test committed first (passes against current blocklist as a guard), then the bats rewrite, then the changelog. Refs: #630
Factor a single devTools list as the source of truth shared by the dev-shell now and the image later. Absorb the #545 agent-CLI toolkit (rg, fd, bat, eza, delta, lazygit, zoxide, starship, freeze, expect, neovim, claude). Pin nixpkgs to nixos-25.05 and overlay fast-movers (uv, gh, claude-code) from nixpkgs-unstable. Add reusable lib.mkProjectShell, overlays.default and a packages.devcontainerImage stub, and refresh flake.lock. Refs: #631
Build the flake dev-shell and push its closure to the vig-os Cachix cache via cachix/install-nix-action (upstream CppNix) + cachix/cachix-action. The job is a standalone, non-required workflow with continue-on-error so it never affects the existing CI gate, and adds workflow_dispatch for on-branch validation. Refs: #631
## Summary Implements **T1.1 (#631)** — collapses the flake dev-shell and the (future) image tool list into a single `devTools` source of truth and lays the reusable flake outputs the rest of the Nix track depends on. ### What changed - **Single `devTools` SSoT** in `flake.nix` — shared basis for the dev-shell now and the image later (#634). Absorbs the #545 agent-CLI toolkit (`rg`, `fd`, `bat`, `eza`, `delta`, `lazygit`, `zoxide`, `starship`, `freeze`, `expect`, `nvim`) plus `claude`, on top of the existing toolchain (`just`, `git`, `gh`, `uv`, `nodejs`, `jq`, `tmux`, `shellcheck`, `hadolint`, `taplo`, `podman`). - **Channel switch** — `nixpkgs` pinned to `nixos-25.05`; new `nixpkgs-unstable` input overlaid **only** for fast-movers `uv`, `gh`, `claude-code`. `flake.lock` refreshed. - **Reusable outputs** — `lib.mkProjectShell` (downstream repos build the shared shell + extra packages), `overlays.default` (the fast-mover overlay), and a `packages.devcontainerImage` **stub** (placeholder; real image is T2.1/#634). - **Cachix CI** — new non-blocking `nix-cachix.yml` workflow that builds the dev-shell and pushes its closure to the `vig-os` cache. - **TDD parity test** — `tests/test_flake_devshell.py` reads the binary names straight from the flake (`nix eval .#devShellTools.<system>`) and runs `nix develop -c <tool> <version-flag>` for each, so it can never drift from `devTools`. ### How `claude` is provided `claude` is the **`claude-code`** package from nixpkgs (binary `claude`, via `meta.mainProgram`). It is a fast-mover, so it is sourced from `nixpkgs-unstable` through `overlays.default` (unstable `2.1.x` vs stable `1.0.85`). No custom fetch/overlay-from-scratch needed — it is packaged cleanly in nixpkgs. > Note: nixpkgs `freeze` is an EDR-bypass payload toolkit (optiv/Freeze), **not** the charmbracelet terminal-screenshot tool #545 intended. Used **`charm-freeze`** (binary `freeze`) instead. ### Evaluator choice + rationale **Upstream CppNix** via `cachix/install-nix-action` (SHA-pinned `v31.10.6`). It is the most broadly supported installer, pairs natively with `cachix/cachix-action`, and needs no extra infra. The flake is **installer-agnostic** — swapping to the Lix `lix-installer` or the Determinate installer requires **zero flake changes**, so this decision is cheaply reversible. ### Cachix wiring - `cachix/install-nix-action@…v31.10.6` (flakes enabled) → `cachix/cachix-action@…v17` with `name: ${{ vars.CACHIX_CACHE }}` and `authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}` (token never printed/hardcoded). - Builds `nix develop --profile dev-profile` and pushes `nix path-info -r ./dev-profile | cachix push`. - **Non-blocking:** standalone workflow (not a required check) + `continue-on-error: true` → existing CI is unaffected. - Adds **`workflow_dispatch`** plus a push trigger on the epic branch for on-branch validation. - Actions are SHA-pinned per `check-action-pins` policy. ### Verification (local, against the public `vig-os` cache) - `nix flake check` → **all checks passed** (`devShellTools` shows only a benign "unknown flake output" warning). - `tests/test_flake_devshell.py` → **2 passed** — every one of the 23 tools runs inside `nix develop` (incl. `claude --version`, `nvim --version`, `rg --version`, `freeze --version`, `tmux -V`, `expect -v`). - `just precommit` → **all hooks pass** (incl. `check-action-pins`, agent-identity, yamllint). ### TDD trail - `test(nix):` failing parity test committed first (RED). - `feat(nix):` flake SSoT + outputs making it pass (GREEN). - `ci(nix):` non-blocking Cachix workflow. Refs: #631
Switch .envrc from bare `use flake` to nix-direnv so the dev-shell evaluation is GC-rooted and cached under .direnv/, making re-entry instant and protecting the closure from garbage collection. nix-direnv self-bootstraps on first `direnv allow` and falls back to bare `use flake` when unavailable. Document the clone -> `direnv allow` onboarding flow, the vig-os Cachix substituter (binary fetch instead of from-source build on first allow), and enabling the nix-command/flakes experimental features in the CONTRIBUTE.md.j2 source template; regenerate with `just docs`. Folds in the Nix-flake-as-alternative-dev-setup documentation request. Refs: #633
The integration suite scaffolds from the freshly-built image but `devcontainer up` resolves the runtime image from DEVCONTAINER_VERSION, so it validates fresh scaffolding inside the stale published image. Add a regression test asserting the running devcontainer uses the image under test (TEST_CONTAINER_TAG). Refs: #701
The devcontainer_up/devcontainer_with_sidecar fixtures scaffolded a workspace from the freshly-built image but let `devcontainer up` resolve the runtime image from DEVCONTAINER_VERSION (the published release baked into the scaffolded .vig-os/.env), so the suite validated fresh scaffolding inside a stale image. Export DEVCONTAINER_VERSION = TEST_CONTAINER_TAG; compose reads the shell environment over .env, so the scaffolded docker-compose.yml resolves the build under test, and the in-test `devcontainer exec` calls inherit it. Refs: #701
post-create.sh ran an unguarded `sed` on the venv activate script before `just sync`. The Debian image baked that venv at build time, but the Nix image populates /root/assets/workspace/.venv during post-create, so the script did not exist yet and post-create aborted (exit 2), failing `devcontainer up`. Move the prompt rewrite after `just sync` and guard it on the file's existence. uv writes the prompt as the venv parent's basename, so rewrite the VIRTUAL_ENV_PROMPT assignment directly instead of substituting the no-longer-present "template-project" literal. Refs: #701
The #697 decoupling shipped the scaffolded ruff/ruff-format/typos hooks as self-contained upstream (manylinux) hooks because the integration suite still ran the published Debian image, whose PATH lacked those tools. Now that the suite runs the freshly-built Nix image -- whose non-FHS userland cannot execute those manylinux binaries -- the workaround breaks the in-container `git commit`. Restore the repo's repo-local language:system hooks in the scaffold (resolved from the image's baked devTools), and drop the now-unused ReplacePrecommitRepoBlock transform and its tests. Refs: #701
Explain in tests/README.md how TEST_CONTAINER_TAG selects the image under test and why the devcontainer fixtures override DEVCONTAINER_VERSION so compose uses it. Record the fixes under CHANGELOG Unreleased. Refs: #701
…702) ## Description The integration suite scaffolded a workspace from the freshly-built image (`TEST_CONTAINER_TAG`) but then ran `devcontainer up` from whatever `DEVCONTAINER_VERSION` resolved to — the published `0.3.9`. So it validated *fresh scaffolding inside a stale image*, not the image under test. Pointing it at the fresh Nix image then surfaced real bring-up failures. This fixes all of it. Closes the gap tracked in #701. ## Type of Change - [x] `fix` -- Bug fix ## Changes Made - **Run the image under test.** `devcontainer_up`/`devcontainer_with_sidecar` now export `DEVCONTAINER_VERSION=TEST_CONTAINER_TAG`. Compose resolves shell env over `.env`, so the scaffolded `docker-compose.yml` (`image: …:${DEVCONTAINER_VERSION:-latest}`) selects the build under test, and the in-test `devcontainer exec` calls inherit it. Added `test_devcontainer_runs_image_under_test` asserting the running container's image. - **Bring the Nix image up cleanly.** `post-create.sh` ran an unguarded `sed` on the venv `activate` script *before* `just sync`. The Debian image baked that venv at build time; the Nix image creates it during post-create, so the script didn't exist yet and post-create aborted (exit 2). Moved the prompt rewrite after `just sync`, guarded it, and rewrote the `VIRTUAL_ENV_PROMPT` assignment directly (uv sets the prompt to the venv parent's basename, so the old `template-project` literal is never present). - **Scaffold ruff/ruff-format/typos as `language: system`.** Reverted the #697 decoupling: those scaffolded hooks pulled upstream manylinux binaries, which the non-FHS Nix userland cannot execute (`No such file or directory` on the ELF interpreter), breaking the in-container `git commit`. They now resolve from the image's baked `devTools`, like the repo's own config. Removed the now-unused `ReplacePrecommitRepoBlock` sync-manifest transform and its tests. - **Docs.** Documented the image-selection behaviour in `tests/README.md`. ## Changelog Entry ### Fixed - **Integration tests now exercise the freshly-built image, not the published `DEVCONTAINER_VERSION`** ([#701](#701)) - The fixtures export `DEVCONTAINER_VERSION=TEST_CONTAINER_TAG` so compose resolves the build under test; added `test_devcontainer_runs_image_under_test` - Guarded/moved the `post-create.sh` venv-prompt `sed` so the Nix image comes up cleanly under `devcontainer up` - Reverted the [#697](#697) scaffold decoupling: scaffolded `ruff`/`ruff-format`/`typos` are `language: system` again; removed the unused `ReplacePrecommitRepoBlock` transform ## Testing - [x] Tests pass locally (`just test`) - [x] Manual testing performed (describe below) ### Manual Testing Details Run against the freshly-built Nix image (`TEST_CONTAINER_TAG=dev`, podman socket + standalone `docker-compose` as CI provides): - `tests/test_integration.py`: **123 passed, 7 skipped** (skips are host-environmental: SSH signing config + sidecar compose), 0 failed - `tests/test_image.py`: **64 passed** - `tests/test_transforms.py` + `tests/test_flake_devshell.py`: pass - `test_devcontainer_runs_image_under_test` confirmed RED against `0.3.9` → GREEN against `dev` - `test_pre_commit_hook` exercises the in-container ruff+typos run that previously failed on the Nix image — now passes. `test_git_commit_ssh_signature` skips locally (no host SSH signing config) but runs in CI's test-integration action. ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have updated the documentation accordingly - [x] I have updated `CHANGELOG.md` in the `[Unreleased]` section (and pasted the entry above) - [x] My changes generate no new warnings or errors - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes ## Additional Notes Sub-issue of the Nix migration epic #625; targets `feature/625-nix-claude-migration`. The `install.sh` host-side `/etc/os-release` probe mentioned in the issue is not exercised by `devcontainer up` (it has a `linux` fallback) and is left unchanged. Refs: #701
The #698 LD_LIBRARY_PATH libstdc++ injection is only needed and ABI-safe on NixOS. Gate the two injection-presence tests to NixOS and add an FHS-only guard asserting the Nix C++ runtime is not exposed on LD_LIBRARY_PATH (it breaks host binaries with GLIBC_ABI_DT_X86_64_PLT). Fails until the dev-shell gate lands. Refs: #703
…ixOS The #698 dev-shell exported the Nix C++ runtime (stdenv.cc.cc.lib, linked against glibc 2.42) onto LD_LIBRARY_PATH unconditionally. On an FHS host with an older system glibc (Ubuntu 24.04 = 2.39) that libstdc++ leaks into host binaries — every just recipe's '#!/usr/bin/env bash', and anything an /etc/ld.so.preload agent pulls libstdc++ into — dragging in the Nix libm.so.6 and aborting with 'GLIBC_ABI_DT_X86_64_PLT not found'. Inject only on NixOS ([ -e /etc/NIXOS ]), where it is both required and ABI-safe; FHS hosts resolve libstdc++ from the system loader, so the injection is a no-op and nothing leaks. Refs: #703
…ixOS (#704) ## Description Closes #703. Fixes a regression from #698 that broke **every `just` recipe inside `nix develop` on non-NixOS (FHS) hosts** with: ``` /usr/bin/env: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_ABI_DT_X86_64_PLT' not found (required by /nix/store/…-glibc-2.42-67/lib/libm.so.6) ``` Root cause: #698 exported `${stdenv.cc.cc.lib}/lib` (the Nix C++ runtime, linked against **glibc 2.42**) onto the dev-shell `LD_LIBRARY_PATH` unconditionally — so the pymarkdown pre-commit wheel could resolve `libstdc++.so.6`. On an FHS host whose **system glibc is older** (Ubuntu 24.04 = 2.39), that Nix `libstdc++` is pulled into host binaries — every `just` recipe runs via `#!/usr/bin/env bash`, and on hosts with an `/etc/ld.so.preload` agent that links `libstdc++` it reaches `/usr/bin/env` directly — dragging in the Nix `libm.so.6`, which requires the `GLIBC_ABI_DT_X86_64_PLT` symbol version the host's 2.39 `libc` does not export. It worked on NixOS only because there the system glibc *is* the Nix glibc. ## Type of Change - [x] `fix` -- Bug fix ## Changes Made - `flake.nix` (`mkProjectShell`): inject the Nix C++ runtime onto `LD_LIBRARY_PATH` **only on NixOS** (`[ -e /etc/NIXOS ]`), where it is both *required* (libstdc++ is off the default loader path) and *ABI-safe* (system glibc is the Nix glibc). On FHS hosts the system loader already resolves `libstdc++`, so the injection is a no-op and nothing leaks into host binaries. - `tests/test_flake_devshell.py`: the #698 injection-presence tests are gated to NixOS, and a new FHS-only `test_devshell_no_nix_cxx_runtime_leak_on_fhs_host` asserts the Nix C++ runtime is **not** exposed on `LD_LIBRARY_PATH` (the RED anchor for this fix). - `CHANGELOG.md` (+ synced workspace copy): `### Fixed` entry. ## Changelog Entry ``` ### Fixed - **Nix dev-shell no longer breaks `just` on non-NixOS hosts (Nix C++ runtime leaked onto `LD_LIBRARY_PATH`)** ([#703](#703)) - The #698 fix exported `${stdenv.cc.cc.lib}/lib` (the Nix C++ runtime, linked against glibc 2.42) onto the dev-shell `LD_LIBRARY_PATH` unconditionally. On an FHS host whose system glibc is older (e.g. Ubuntu 24.04 ships 2.39), that `libstdc++` is pulled into host binaries … aborting with `version 'GLIBC_ABI_DT_X86_64_PLT' not found`, so every `just` recipe failed inside `nix develop` - `mkProjectShell` now injects the Nix C++ runtime onto `LD_LIBRARY_PATH` only on NixOS (`[ -e /etc/NIXOS ]`) … FHS hosts resolve `libstdc++` from the system loader, so the injection is a no-op there and nothing leaks. The #698 dev-shell parity tests are gated to NixOS and an FHS leak-guard was added ``` ## Testing - [x] Tests pass locally — `pytest tests/test_flake_devshell.py`: **8 passed, 2 skipped** (the two NixOS-only injection tests skip on this FHS host; the leak-guard passes). - [x] Manual testing performed (below) ### Manual Testing Details On Ubuntu 24.04 (glibc 2.39), inside `nix develop`: - Before: `LD_LIBRARY_PATH` carried `…/gcc-15.2.0-lib/lib`; `just build` / `just test-bats` aborted with the GLIBC error above. - After: `LD_LIBRARY_PATH` is empty; `/usr/bin/env bash` runs; `just lint` passes and `just build` proceeds into the real Nix image build. - TDD: the leak-guard test was committed RED first (`a818fb7`), then the flake gate made it GREEN (`e27962a`). > CI does not auto-run for PRs into the epic branch (`ci.yml` triggers on PRs to > `dev`/`release/**`/`main`), so the `project` suite is dispatched manually on > this head branch. ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have updated `CHANGELOG.md` in the `[Unreleased]` section (and pasted the entry above) - [x] My changes generate no new warnings or errors - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes
This was referenced Jun 25, 2026
The buildLayeredImage tag was a stale, branch-specific WIP value (nix-wt634). Align it with the disposable discovery tag the CI workflow documents as INDEX_TAG=nix-dev (.github/workflows/nix-image.yml). The versioned / :latest cutover tag is out of scope (#639). Skip TDD: build constant, no behavioral surface. Refs: #705
Remove the repeated "Cleaning" from the clean-test-containers echo so it reads "Cleaning up lingering test containers...". Skip TDD: cosmetic/comment-only, no behavioral surface. Skip CHANGELOG: no user-visible impact (echo wording only). Refs: #710
The Nix image ships CPython 3.14, but the TESTING template still listed Python 3.12. Update the template and regenerate TESTING.md. Skip TDD: documentation template. Skip CHANGELOG: minor doc accuracy fix, no user-visible behaviour change. Refs: #709
The hook id sync-manifest was defined twice. The second block only ran when scripts/manifest.toml changed, which would miss drift in other synced source files. Keep the broad always-run hook and remove the narrow duplicate. Skip TDD: pre-commit config de-duplication, no behavioral surface. Refs: #707
The .vscode/settings.json manifest entry rewrote the interpreter path to
/opt/venv/bin/python3, the decommissioned Debian image path. The Nix image
has no /opt/venv, so downstream projects got a broken VS Code interpreter.
Remove the Sed transform so the workspace-relative
${workspaceFolder}/.venv/bin/python3 (resolved against the opened project,
where uv creates .venv) passes through unchanged, and regenerate the synced
workspace asset.
Refs: #706
The added TestWorkspaceInterpreterPath was committed via an env workaround that bypassed ruff-format; CI's flake-sourced ruff (0.15.x) flags it. Refs: #706
Align ruff with requires-python >=3.14. The flake-sourced ruff-format (0.15.x) then applies PEP 758 to the py314 target, dropping the parentheses on multi-exception except clauses in tests/conftest.py and packages/vig-utils/tests/test_claude_ssot.py. Skip TDD: ruff lint/format configuration. Refs: #708
## Summary `flake.nix` baked a stale, branch/issue-specific WIP tag `nix-wt634` into `dockerTools.buildLayeredImage`. This aligns the image tag with the disposable discovery tag the CI workflow already documents as `INDEX_TAG: nix-dev` (`.github/workflows/nix-image.yml`), so the flake and CI agree on one tag. - Changed `tag = "nix-wt634"` -> `tag = "nix-dev"`, with a clarifying comment. - Does **not** touch the versioned / `:latest` cutover tags, which are tracked separately in #639. ## Verification - `grep -rn 'nix-wt' . --exclude-dir=.git --exclude-dir=docs` returns nothing. - `nixfmt --check flake.nix` clean; `nix flake check --no-build` -> all checks passed. Skip TDD: build constant, no behavioral surface. No CHANGELOG entry: internal build plumbing, not user-visible. Closes #705
Purely cosmetic, non-functional cleanup for #710. ## Changes - **justfile** (`clean-test-containers` recipe): removed the duplicated word so the echo reads `Cleaning up lingering test containers...` instead of `Cleaning Cleaning up ...`. ## Left alone (deliberately) - `assets/workspace/.devcontainer/scripts/post-create.sh`: comments are generic and not Debian-specific; nothing is factually wrong post-Nix. - `tests/test_image.py`: the `EXPECTED_VERSIONS` block carries Debian-era comments ("from apt package", "Containerfile", install-method notes), but that whole block (values + comments) is being rewritten by the active Nix-migration work, so touching the comment text here would overlap with another PR. Per the "when in doubt, skip" guidance, left untouched. Skip TDD: cosmetic/comment-only, no behavioral surface. Skip CHANGELOG: no user-visible impact (echo wording only). Closes #710
The Nix image ships CPython 3.14, but the auto-generated TESTING.md (via docs/templates/TESTING.md.j2) still listed Python 3.12. Fixed the template and regenerated TESTING.md so the docs match the shipped interpreter (README was already correct). Closes #709
Kept the broad always-run sync-manifest hook (no `files:` filter, so it runs whenever any synced source file changes). Removed the duplicate `sync-manifest` block whose narrow `files: ^scripts/manifest\.toml$` filter would miss drift in other synced sources (CHANGELOG.md, .vscode/settings.json, .pre-commit-config.yaml, etc.). Closes #707
Align ruff `target-version` with the project's `requires-python = ">=3.14,<3.15"` so lint and formatting target the real runtime instead of the stale `py312` value. Closes #708
…env (#716) `/opt/venv` is the decommissioned Debian image path; the Nix image has no such directory. The `.vscode/settings.json` manifest entry rewrote `python.defaultInterpreterPath` to `/opt/venv/bin/python3`, shipping a broken VS Code interpreter to downstream projects. This removes the Sed transform so the source's workspace-relative `${workspaceFolder}/.venv/bin/python3` passes through unchanged (VS Code resolves `${workspaceFolder}` to the opened project, where `uv` creates `.venv`), and regenerates the synced workspace asset. Added a behavioral test that runs the real manifest sync and asserts the interpreter path is workspace-relative and never `/opt/venv`. Closes #706
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands epic #625: the devcontainer is now built and provisioned entirely by Nix, with a Claude-native agent setup. The Debian/
apt+ Cursor stack is gone.What changed (by track)
.claude/is the SSoT for rules/skills; worktree pipelines driveclaude; commit-msg blocklist still blocks naming any AI agent.flake.nixis the single toolchain source (devTools,lib.mkProjectShell,overlays.default); CI provisions from it; nix-direnv onboarding.dockerTools.buildLayeredImage, bit-reproducible, multi-arch (amd64+arm64); the project Python toolchain (vig-utils,ruff,bandit,pre-commit,pip-licenses) is baked from Nix; full testinfra suite passes (63/63).vulnix(nixpkgs-native) is the primary nightly scanner viavulnix-gate+.vulnixignore; Trivy stays for a CycloneDX SBOM.nixos-26.05baseline; downstream minimal flake stub + nix2container pattern; install/init--mode=devcontainer|direnv|bothpicker; Debian path decommissioned (Containerfile,scripts/build.sh, hadolint,type=gha, Debian Trivy job, renovate dockerfile manager all removed).Acceptance criteria
.claude/the SSoT for rules & skills #626–T4.4 — Decommission the Debian path #642 + bugfix: Nix image scaffolds dangling, read-only symlinks into a new workspace #664/T2.4 — Nix image toolchain parity: pass the full testinfra suite #666).grep -ri cursor→ only CHANGELOG history + the intentional commit-msg blocklist (+ its tests).buildLayeredImage, epoch timestamp).Publish / rollback
:latestflips to Nix at the next release fromdev(release.yml, now Nix-only). Rollback, if ever needed, is to branch from the last Debian-built tagged release.Closes #625. Supersedes #27, #255, #545, #602, #521, #604, #144, #153, #231.